#!/bin/sh
# Copyright (C) 2016 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
usage () { echo Usage: `basename $0` '[-h|-u] [-i] [-r ref] [--] command...'; }

help () {
    usage
    # Example data:
    [ "$LAST" ] || LAST=v5.9.0
    [ "$NEXT" ] || NEXT=5.10
    [ "$TASK" ] || TASK=QTBUG-5678
    ME=`basename $0`
    cat <<EOF

Run from the top-level (qt5) directory of a full Qt checkout with the
module-set of an imminent release.  For API change review generation,
check out .gitmodules on the reference version, so as to use its list
of modules (and skip modules that were in preview back then).  Give
this script a command to run in each module; the purpose of this
script is to run api-review-gen on each; but it also serves to prepare
for this and later to run a git push to send the results to Gerrit for
review.  See examples below and output of ./api-review-gen -h for
further details.

Although this script can be used to run a general command in each
module, its selection of modules is specifically geared towards the
particular task of generating API change reviews, under the conditions
described above; a general module iterator might one day replace it,
but please don't try to evolve this into that.

Options:

 -u
 --usage   Print simple usage summary (first line above) and exit.

 -h
 --help    Print this help and exit.

 -i
 --ignored
           Invert the selection on status, to get the modules that
           would normally be ignored, but that otherwise would qualify
           for review.

 -r ref
 --require ref
           Limit to modules that have a specific branch or tag.
           May be used repeatedly to require several refs.

 --        End option processing.

Arguments:

 command...
          The command to run in each relevant module, along with its
          arguments.

Several --require options may be used; for example, one for a prior
release tag, another for an imminent release branch.  Note that, if a
relevant module is missing (not cloned), this script silently skips
it; you do need to have a suitable full work tree.

By way of example, here's the work-flow for producing an API change
review between $LAST and $NEXT to be tracked by Jira issue $TASK (you
can set LAST, NEXT and TASK in the environment to configure the
examples), assuming qtqa/scripts/api-review is in your PATH:

 Get qt5 up to date, but with the old .gitmodules:
 $ git checkout $NEXT && git pull --ff-only
 $ git checkout $LAST -- .gitmodules

 Check status of all relevant modules:
 $ $ME git status
 You'll need all your working trees clean before you proceed.

 Get each potentially relevant module's origin up to date:
 $ $ME git fetch origin

 Make sure each has the new branch available to check out:
 $ $ME -r origin/$NEXT \\
        git fetch origin $NEXT:$NEXT '||' git pull --ff-only

 Generate review branches (but see notes following):
 $ $ME -r $LAST -r $NEXT api-review-gen -t $TASK $LAST $NEXT

 Add a --amend before the -t in that to update review branches to
 reflect changes in imminent release (e.g. fixes for an earlier round
 of review) after updating $NEXT as above.  If the $NEXT branch name
 isn't the imminent release's version (e.g. if the release hasn't yet
 branched out from dev), see the --release option to api-review-gen.

 Examine residual diff (should all be boring):
 $ $ME -r api-review-$LAST-$NEXT git diff -b -D

 If any of that isn't boring, you can git add -p the relevant files
 and git commit --amend to include the non-boring parts of the files.
 In such a case, it is a good idea to work out why resetboring.py
 thought it was boring and fix it, if possible, to avoid similar
 errors in future.

 When doing an amend, git fetch the currently live patch-set of the
 review in each sub-module (sadly not something we can automate with
 this script) and also git diff -b -D FETCH_HEAD HEAD - if others have
 added files to the review (as can happen with private headers that
 define QML API), they'll show up in this diff. You need to check such
 files out on $NEXT and add them to the newly amended commit. You can
 run resetboring.py (without --disclaim) to unstage any boring changes
 to such files, before committing such additions.

 Clear away residual diff (after verifying it's all boring):
 $ $ME -r api-review-$LAST-$NEXT git reset --hard HEAD

 Remove review branch where we committed nothing:
 $ $ME -r api-review-$LAST-$NEXT \\
        if git diff --quiet $LAST api-review-$LAST-$NEXT ';' \\
        then git reset --hard HEAD '&&' \\
             git checkout $NEXT '&&' \\
             git branch -D api-review-$LAST-$NEXT ';' fi
 Note the use of quoted shell metacharacters; $ME shall
 eval the command, ensuring they do their proper job.

 Now we must discard any modules that aren't part of the coming
 release, for which we need the current .gitmodules rather than the
 old one:
 $ git checkout $NEXT -- .gitmodules

 Clear review branches from any modules to be ignored:
 $ $ME -i -r api-review-$LAST-$NEXT \\
        git checkout $NEXT '&&' git branch -D api-review-$LAST-$NEXT

 Examine changes (mostly interesting) to be reviewed:
 $ $ME -r api-review-$LAST-$NEXT git log -p $LAST..

 Push to gerrit:
 $ $ME -r api-review-$LAST-$NEXT \\
     git push gerrit HEAD:refs/for/$NEXT%topic=api-change-review-$NEXT

 When $LAST has changed (only relevant when using a branch here),
 rebase review branches onto it:
 $ $ME -r api-review-$LAST-$NEXT git rebase $LAST api-review-$LAST-$NEXT

Note that all of the above presumes you have an up-to-date module set
checked out; if you have not run qt5/init-repository recently, it may
be prudent to do so before you start. Some modules may have been added
or changed status since you last ran it.
EOF
}
warn () { echo "$@" >&2; }
die () { warn "$@"; exit 1; }
banner () { echo; echo "====== $@ ======"; echo; }

# Convert each .gitmodules stanza into a single line:
modules () {
    sed -e 's/^\[submodule/\v[submodule/' <.gitmodules | tr '\n\v' '\f\n'
    echo
}

# Parse a single linearised stanza; discard if boring (or, when INVERT
# is set, if interesting), else extract its directory name:
INVERT=
vetcheck () {
    # Can't return from within the while loop; piping input to it has
    # made it a sub-shell. (That also precludes assigning any
    # variables within the loop; values would be forgotten after.)
    echo "$@" | tr '\f' '\n' | while read name op value
    do
        [ "$op" = '=' ] || continue
        case "$name" in
            status)
                # No BC/SC promises in preview; and we
                # don't care about obsolete or ignore:
                case "$value" in
                    essential|addon|deprecated) [ -z "$INVERT" ] || echo no ;;
                    preview|obsolete|ignore) [ -n "$INVERT" ] || echo no ;;
                    *) warn "Unrecognized module status: $value (treated as active)"
                        [ -z "$INVERT" ] || echo no
                        ;;
                esac;;
            # repotools has qt = false
            qt) [ "$value" = false ] && echo no ;;
            # non-versioned modules aren't relevant to us:
            branch) [ "$value" = master ] && echo no ;;
        esac
    done
}

vet () {
    # First pass: filter out modules we don't want:
    if vetcheck "$@" | grep -q no
    then return
    fi
    echo "$@" | grep -w 'status *= *' | \
        tr '\f' '\n' | grep 'path *=' | cut -d = -f 2
}

# Re-echo a module if it has all required refs:
checkrefs () {
    for ref in $REFS
    # Use rev-parse --verify to test whether $ref exists in this repo:
    do (cd "$1" && git rev-parse -q --verify "$ref^{commit}" >/dev/null 2>&1) || return
    done
    echo "$1"
}

# List the API-relevant modules:
relevant () {
    # Select released Qt modules:
    modules | while read stanza
    do vet "$stanza"
    done | while read path
    # Only those with src/ and a CMakeLists.txt matter:
    do if [ -d "$path/src" -a -f "$path/CMakeLists.txt" ]
       # Filter on the desired refs (if any):
       then checkrefs "$path"
       fi
    done
}

REFS=
while [ $# -gt 0 ]
do
    case "$1" in
        -u|--usage) usage; exit 0;;
        -h|--help) help; exit 0;;
        -i|--ignored) INVERT=yes; shift;;
        -r|--require) REFS="$REFS $2"; shift 2;;
        --) shift; break;;
        -*) usage >&2; die "Unrecognised option: $1";;
        *) break;;
    esac
done
[ -e .gitmodules ] || die "I must be run in the top-level (qt5) directory"
if [ $# -eq 0 ]
then
    usage >&2
    die "You need to supply a command to run in each module !"
fi

relevant | while read dir
do (cd "$dir" && banner "$dir" && eval "$@") || warn "Failed ($?) in $dir: $@"
done
